/**
 * @file    assembler.c
 * @author  Thomas Di'Meco and Matthieu Maugard
 * @brief   Parse and assemble a procsi program
 */

#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <stdarg.h>

#include "assembler.h"
#include "memory.h"
#include "instructions.h"

#define ASSEMBLY_ERROR    1     ///< Error macro for assembly error
#define ASSEMBLY_NO_ERROR 0     ///< Error macro for no assembly error


// Private members
FILE *  err_stream;
int     current_line;
int     mem_count = 0;
int     offsets [MEMORY_SIZE]    = {0};
int16_t adrlist [MEMORY_SIZE/10] = {0};

int     parse_line (char * line);
int     get_operand (char * op, int16_t * mode, int16_t * value);
void    write_mem (word_t word);
void    add_memadr (int16_t adr);
char *  safe_fgets (char * str, int num, FILE * stream);
void    print_err (const char * format, ...);


/**
 * Assemble the given file by parsing each instruction
 * and store the result into memory
 * @param filename The file to assemble
 * @param err_file The error output stream
 * @return ASSEMBLY_ERROR if an error occured,
 *         ASSEMBLY_NO_ERROR otherwise
 */
int assemble (const char * filename, FILE * err_file)
{
    // Override local error stream
    err_stream = err_file;

    // Try to open file
    FILE * file = fopen(filename, "r");

    if (file == NULL)
    {
        print_err("unable to open '%s'", filename);
        return ASSEMBLY_ERROR;
    }

    // Parse each line
    current_line = 1;
    char line [100];

    while (safe_fgets(line, 100, file))
    {
        if (parse_line(line))
        {
            // Close file and return with error
            fclose(file);
            return ASSEMBLY_ERROR;
        }

        current_line++;
    }

    // Close file
    fclose(file);

    // Resolve addresses offsets
    int i;
    for (i = 0; adrlist[i] != 0; ++i)
    {
        int16_t adr = mem[adrlist[i]].raw;
        mem[adrlist[i]].raw = adr + offsets[adr];
    }

    return ASSEMBLY_NO_ERROR;
}


/**
 * Parse the given line and store the corresponding instruction
 * into memory
 * @param line The line string
 * @return ASSEMBLY_ERROR if an error occured,
 *         ASSEMBLY_NO_ERROR otherwise
 */
int parse_line (char * line)
{
    int     op = 0;
    int     nword = 0;
    word_t  word  = EMPTY_WORD;
    word_t  word2 = EMPTY_WORD;
    word_t  word3 = EMPTY_WORD;
    int16_t dest_mode = 0;
    int16_t src_mode  = 0;

    static int current_inst = 0;

    // Tokenize line
    const char * delimiters = " \t,";
    char * t = strtok(line, delimiters);

    // For each token
    while (t != NULL)
    {
        // Check comment token
        if (strlen(t) >= 2 && !strncmp(t, "--", 2)) break;

        // Fetch instruction
        if (op == 0)
        {
            nword++;
            int16_t codeop = get_instruction(t);

            // Unknown instruction
            if (codeop < 0)
            {
                print_err("unknown instruction '%s' at line %d", t, current_line);
                return ASSEMBLY_ERROR;
            }

            word.inst.codeop = codeop;
        }
        // Fetch first or second operand if exists
        else if (op == 1 || op == 2)
        {
            int16_t mode, value;

            // Get operand mode and value and check errors
            if (get_operand(t, &mode, &value))
            {
                print_err("invalid operand '%s' at line %d", t, current_line);
                return ASSEMBLY_ERROR;
            }

            // If direct or immediate mode
            if (mode == DIR || mode == IMM)
            {
                if (nword == 1) word2.raw = value;
                else            word3.raw = value;

                nword++;

                // Add current address to the list in order to resolve the offsets later
                if ( word.inst.codeop == JMP ||
                     word.inst.codeop == JEQ ||
                     word.inst.codeop == CALL )
                {
                    add_memadr(mem_count + nword - 1);
                }
            }
            // If indirect or register mode
            else
            {
                if (op == 1)
                {
                    // Set both destination and source registers to avoid particular cases
                    word.inst.dest = word.inst.src = value;
                }
                else
                    word.inst.src = value;
            }

            // Create the instruction mode from destination and source modes
            if (op == 1)
            {
                // Particular cases
                switch (word.inst.codeop)
                {
                    case POP:
                        dest_mode = mode;
                        src_mode  = REG; // Unused
                        word.inst.mode = get_mode(dest_mode, src_mode);
                        break;

                    case JMP:
                    case JEQ:
                    case CALL:
                    case PUSH:
                        dest_mode = REG; // Unused
                        src_mode  = mode;
                        word.inst.mode = get_mode(dest_mode, src_mode);
                        break;

                    default:
                        dest_mode = mode;
                        break;
                }
            }
            else
            {
                src_mode = mode;
                word.inst.mode = get_mode(dest_mode, src_mode);
            }
        }
        // Wrong number of operand
        else
        {
            print_err("invalid operand '%s' at line %d", t, current_line);
            return ASSEMBLY_ERROR;
        }

        // Next token
        t = strtok(NULL, delimiters);
        op++;
    }

    // Save offset count for the current instruction
    if (nword >= 1)
    {
        offsets[current_inst+1] = offsets[current_inst] + nword - 1;
        current_inst++;
    }

    // Write words into memory
    if (nword >= 1) write_mem(word );
    if (nword >= 2) write_mem(word2);
    if (nword >= 3) write_mem(word3);

    return ASSEMBLY_NO_ERROR;
}


/**
 * Returns the operand mode and value from the given operand string
 * @param op    The operand string of an instruction
 * @param mode  The mode pointer to be edited by this function
 * @param value The value pointer to be edited by this function
 * @return ASSEMBLY_ERROR if an error occured,
 *         ASSEMBLY_NO_ERROR otherwise
 */
int get_operand (char * op, int16_t * mode, int16_t * value)
{
    // Case : Register
    if (!strncmp(op, "R", 1))
    {
        *mode  = REG;
        *value = atoi(op+1);
    }
    // Case : Immediate
    else if (!strncmp(op, "#", 1))
    {
        *mode  = IMM;
        *value = atoi(op+1);
    }
    // Case : Direct
    else if (!strncmp(op, "[", 1))
    {
        // Delete the final ]
        int i;
        for (i = 0; op[i] != '\0'; ++i);
        op[i-1] = '\0';

        *mode  = DIR;
        *value = atoi(op+1);
    }
    // Case : Indirect
    else if (!strncmp(op, "[R", 2))
    {
        // Delete the final ]
        int i;
        for (i = 0; op[i] != '\0'; ++i);
        op[i-1] = '\0';

        *mode  = IND;
        *value = atoi(op+2);
    }
    // Error
    else
        return ASSEMBLY_ERROR;

    return ASSEMBLY_NO_ERROR;
}


/**
 * Write the given word into the next free space of memory
 * @param word The word to store into memory
 */
void write_mem (word_t word)
{
    if (!is_mem(mem_count))
        print_err("reached memory limit at line %d", current_line);
    else
        mem[mem_count++] = word;
}


/**
 * Add an address of memory into the adrlist array.
 * These address corresponds to immediate value that
 * will be corrected by the offset corrector later
 * @param adr The address to add
 */
void add_memadr (int16_t adr)
{
    static int adr_count = 0;

    if (adr_count < MEMORY_SIZE/10)
        adrlist[adr_count++] = adr;
}


/**
 * Safe get a line into the given stream and discard excess at end of string
 * @param str    Pointer to an array of chars where the string read is stored
 * @param num    Maximum number of characters to be read
 *               (including the final null-character).
 *               Usually, the length of the array passed as str is used
 * @param stream Pointer to a FILE object that identifies the stream where
 *               characters are read from
 * @return On success, the function returns the same str parameter.
           If the End-of-File is encountered and no characters have been read,
           the contents of str remain unchanged and a null pointer is returned.
           If an error occurs, a null pointer is returned.
           Use either ferror or feof to check whether an error happened
           or the End-of-File was reached.
 */
char * safe_fgets (char * str, int num, FILE * stream)
{
    char * ret = fgets(str, num, stream);

    if (ret != NULL)
    {
        // Clear end of line
        char * p = strchr(str, '\n');

        if (p)
            *p = 0;
        else
        {
            int c;
            while ((c = fgetc(stream)) != '\n' && c != EOF);
        }
    }

    return ret;
}


/**
 * Write an error into the local error stream
 * @param format A formated string
 */
void print_err (const char * format, ...)
{
    va_list ap;
    va_start (ap, format);

    fprintf(err_stream, "Error: ");
    vfprintf(err_stream, format, ap);
    fprintf(err_stream, "\n");

    va_end (ap);
}
